package teleware.soe.core.start;

import com.esri.arcgis.carto.IMapLayerInfos;
import com.esri.arcgis.carto.IMapServer3;
import com.esri.arcgis.carto.IMapServerInfo;
import com.esri.arcgis.interop.AutomationException;
import com.esri.arcgis.server.IServerObjectExtension;
import com.esri.arcgis.server.IServerObjectHelper;
import com.esri.arcgis.system.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import teleware.soe.core.annotation.OperationMapping;
import teleware.soe.core.annotation.RequestParam;
import teleware.soe.core.annotation.ResourceMapping;
import teleware.soe.core.context.ApplicationContext;
import teleware.soe.core.dto.MethodMapping;
import teleware.soe.core.dto.Operations;
import teleware.soe.core.dto.Resources;
import teleware.soe.core.dto.Schema;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Rest SOE 接口的抽象实现
 * <p>
 * IServerObjectExtension方法：这是所有SOE必须支持的强制性接口。
 * 服务器对象使用此接口来管理SOE的生存期，并包括两种方法：init（）和shutdown（）。
 * 服务器对象共同创建SOE，并调用init（）方法，通过服务器对象帮助程序参数将其向服务器对象返回引用。
 * 服务器对象帮助器在服务器对象上实现了弱引用。
 * 该扩展可以在Server Object Helper上保留强引用（例如，在成员变量中），但不应在Server Object上保留强引用。日志条目仅是信息性的，并且是完全可选的。
 * <p>
 * IRESTRequestHandler方法：公开处理针对服务器对象扩展（SOE）的REST请求并提供架构的方法
 * <p>
 * IObjectConstruct方法：只有在ArcGIS Server Manager.中有必要输入属性，使其作为SOE的全局变量时才需要继承，
 * 它提供了一个非常简单的方法construct()，这个方法是在SOE创建之后，进行init()初始化之后调用，可以通过这方法获取输入参数。
 * <p>
 * IObjectActivate方法：这是一个可选的继承接口，除非你需要在服务Request请求之前或之后有逻辑性的操作
 *
 * @author :hongsw
 * @date :2020-12-08 11:32
 */
public abstract class AbstractRestServerObjectExtension implements IServerObjectExtension, IRESTRequestHandler, IObjectConstruct, IObjectActivate {
    /**
     * 提供对持有人的访问，以引用服务器对象。
     */
    protected IServerObjectHelper soHelper;

    /**
     * 提供对访问日志的方法的访问。
     */
    protected ILog serverLog;

    /**
     * 提供对地图图层信息收集界面的访问。
     */
    protected IMapLayerInfos layerInfos;

    /**
     * 资源层次结构
     */
    protected Schema schema;

    protected ApplicationContext applicationContext;

    protected ObjectMapper mapper = new ObjectMapper();

    /**
     * 创建SOE实例后，将调用一次init（）。
     *
     * @param iServerObjectHelper 提供对持有人的访问，以引用服务器对象。
     * @author hongsw
     * @date 2020/12/8 11:20
     **/
    @Override
    public void init(IServerObjectHelper iServerObjectHelper) throws IOException, AutomationException {
        schema = new Schema();
        this.soHelper = iServerObjectHelper;
        this.serverLog = ServerUtilities.getServerLogger();
        serverLog.addMessage(3, 200, "Beginning init in " + this.getClass().getName() + " SOE.");
        IMapServer3 ms = (IMapServer3) this.soHelper.getServerObject();
        IMapServerInfo mapServerInfo = ms.getServerInfo(ms.getDefaultMapName());
        this.layerInfos = mapServerInfo.getMapLayerInfos();
        serverLog.addMessage(3, 200, "Initialized " + this.getClass().getName() + " SOE.");
    }

    /**
     * 这个方法是在SOE创建之后，进行init()初始化之后调用，可以通过这方法获取输入参数
     *
     * @param iPropertySet 提供对成员的访问以管理PropertySet。
     * @author hongsw
     * @date 2020/12/8 11:54
     **/
    @Override
    public void construct(IPropertySet iPropertySet) throws IOException, AutomationException {
        applicationContext = new ApplicationContext(getConfig());
    }

    /**
     * 此方法返回基于REST的SOE的资源层次结构。该层次结构为JSON格式
     *
     * @return java.lang.String
     * @author hongsw
     * @date 2020/12/8 11:32
     **/
    @Override
    public String getSchema() throws IOException, AutomationException {
        Schema schema = new Schema();
        addResource(schema);
        addOperation(schema);
        return mapper.writeValueAsString(schema);
    }

    protected abstract Class<?> getConfig();


    /**
     * 当服务器对象的上下文被关闭并且即将消失时，shutdown（）被调用一次。
     *
     * @author hongsw
     * @date 2020/12/8 11:21
     **/
    @Override
    public void shutdown() throws IOException, AutomationException {
        serverLog.addMessage(3, 200, "Shutting down " + this.getClass().getName() + " SOE.");
        this.soHelper = null;
        this.serverLog = null;
    }

    /**
     * 客户端做一次请求之前执行
     *
     * @author hongsw
     * @date 2020/12/8 11:55
     **/
    @Override
    public void activate() throws IOException, AutomationException {

    }

    /**
     * 客户端请求一次完成执行.
     *
     * @author hongsw
     * @date 2020/12/8 11:55
     **/
    @Override
    public void deactivate() throws IOException, AutomationException {

    }

    /**
     * 每次进行Http请求时执行.
     * 作用：
     * 1.回调资源和操作的方法
     * 2.获取资源在实例级别的描述
     *
     * @param capabilities       一组被资源授权的操作，可以为空字符串
     * @param resourceName       被寻址资源的名称。如果为空，则假定正在处理根资源。
     * @param operationName      被调用的操作的名称。如果为空，则返回资源描述
     * @param operationInput     将输入参数以逗号分隔列表的形式输入到operationName参数指定的操作。
     * @param outputFormat       OutputFormat操作。可能的格式为JSON，HTML，AMF等。
     * @param requestProperties  未知
     * @param responseProperties 通过操作返回的一组键值对，逗号分开
     * @return byte[]
     * @author hongsw
     * @date 2020/12/8 11:23
     **/
    @Override
    public byte[] handleRESTRequest(String capabilities, String resourceName,
                                    String operationName, String operationInput, String outputFormat,
                                    String requestProperties, String[] responseProperties) throws IOException {
        Object result;
        // 资源请求
        if (operationName.length() == 0) {
            // 根路径资源
            if ("".equalsIgnoreCase(resourceName)) {
                result = getRootResource();
            } else {
                // 请求资源
                result = getResource(resourceName);
            }
        } else {
            // 操作请求
            result = getOperation(operationName, operationInput);
        }
        return mapper.writeValueAsString(result).getBytes("UTF-8");
    }

    private Object getOperation(String operationName, String operationInput) throws IOException {
        Object result = null;
        MethodMapping methodMapping = applicationContext.getMappingPool().getOperationMappingPool().get(operationName);
        String beanName = methodMapping.getBeanName();
        Method method = methodMapping.getMethod();
        Map<String, String> map = mapper.readValue(operationInput, Map.class);
        Class<?>[] parameterTypes = method.getParameterTypes();
        Object[] args = new Object[parameterTypes.length];
        int argsMark = 0;
        int index = 0;
        for (Class<?> parameterType : parameterTypes) {
            if (IMapLayerInfos.class.isAssignableFrom(parameterType)) {
                args[argsMark++] = layerInfos;
            }
            if (IServerObjectHelper.class.isAssignableFrom(parameterType)) {
                args[argsMark++] = soHelper;
            }
            Annotation[] parameterAnnotations = method.getParameterAnnotations()[index];
            if (parameterAnnotations.length > 0) {
                for (Annotation parameterAnnotation : parameterAnnotations) {
                    if (RequestParam.class.isAssignableFrom(parameterAnnotation.getClass())) {
                        String key = ((RequestParam) parameterAnnotation).value();
                        args[argsMark++] = map.get(key);
                    }
                }
            }
            index++;
        }
        Object bean = applicationContext.getBean(beanName);
        try {
            result = method.invoke(bean, args);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 返回资源请求
     *
     * @param resourceName 资源请求路径
     * @return java.lang.Object
     * @author hongsw
     * @date 2020/12/14 10:54
     **/
    private Object getResource(String resourceName) {
        Object result = null;
        MethodMapping methodMapping = applicationContext.getMappingPool().getResourceMappingPool().get(resourceName);
        Method method = methodMapping.getMethod();
        String beanName = methodMapping.getBeanName();
        Object bean = applicationContext.getBean(beanName);
        Class<?>[] parameterTypes = method.getParameterTypes();
        Object[] args = new Object[parameterTypes.length];
        int argsMark = 0;
        for (Class<?> parameterType : parameterTypes) {
            if (IMapLayerInfos.class.isAssignableFrom(parameterType)) {
                args[argsMark++] = layerInfos;
            }
            if (IServerObjectHelper.class.isAssignableFrom(parameterType)) {
                args[argsMark++] = soHelper;
            }
        }
        try {
            result = method.invoke(bean, args);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return result;
    }


    /**
     * 根路径资源描述
     *
     * @return Map<String, String>
     * @author hongsw
     * @date 2020/12/10 20:56
     **/
    public Map<String, String> getRootResource() {
        return new HashMap<String, String>(3) {{
            put("SOE名称", "模板名称");
            put("SOE描述", "模板描述");
            put("SOE用法", "模板用法");
        }};
    }


    /**
     * 新增操作
     *
     * @param schema SOE资源-操作层次结构
     * @author hongsw
     * @date 2020/12/14 10:47
     **/
    private void addOperation(Schema schema) {
        ConcurrentHashMap<String, MethodMapping> operationMappingPool = applicationContext.getMappingPool().getOperationMappingPool();
        // 遍历所有的操作映射
        for (Map.Entry<String, MethodMapping> stringMethodMappingEntry : operationMappingPool.entrySet()) {
            MethodMapping methodMapping = operationMappingPool.get(stringMethodMappingEntry.getKey());
            // 映射方法
            Method method = methodMapping.getMethod();
            OperationMapping annotation = method.getAnnotation(OperationMapping.class);
            //拿到当前待执行的方法有哪些参数
            Class<?>[] parameterTypes = method.getParameterTypes();
            String[] args = new String[parameterTypes.length];
            int argsMark = 0;
            int index = 0;
            for (Class<?> ignored : parameterTypes) {
                Annotation[] parameterAnnotations = method.getParameterAnnotations()[index];
                if (parameterAnnotations.length > 0) {
                    for (Annotation parameterAnnotation : parameterAnnotations) {
                        if (RequestParam.class.isAssignableFrom(parameterAnnotation.getClass())) {
                            args[argsMark++] = ((RequestParam) parameterAnnotation).value();
                        }
                    }
                }
                index++;
            }
            String[] newArgs = new String[argsMark];
            if (newArgs.length >= 0) {
                System.arraycopy(args, 0, newArgs, 0, newArgs.length);
            }
            String[] supportedOutputFormats = annotation.supportedOutputFormats().split(",");
            boolean postOnly = annotation.postOnly();
            schema.getOperations().add(new Operations(stringMethodMappingEntry.getKey(), newArgs, supportedOutputFormats, postOnly));
        }
    }

    /**
     * 新增资源结构
     *
     * @param schema SOE资源-操作层次结构
     * @author hongsw
     * @date 2020/12/11 17:54
     **/
    private void addResource(Schema schema) {
        ConcurrentHashMap<String, MethodMapping> resourceMappingPool = applicationContext.getMappingPool().getResourceMappingPool();
        // 遍历所有的资源映射
        for (Map.Entry<String, MethodMapping> stringMethodMappingEntry : resourceMappingPool.entrySet()) {
            MethodMapping methodMapping = resourceMappingPool.get(stringMethodMappingEntry.getKey());
            Method method = methodMapping.getMethod();
            ResourceMapping annotation = method.getAnnotation(ResourceMapping.class);
            String description = annotation.description();
            boolean isCollection = annotation.isCollection();
            boolean isDefaultCollection = annotation.isDefaultCollection();
            schema.getResources().add(new Resources(stringMethodMappingEntry.getKey(), description, isCollection, isDefaultCollection));
        }
    }
}
